/*
 * SoulFire
 * Copyright (C) 2024  AlexProgrammerDE
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package com.soulfiremc.server.grpc;

import com.google.protobuf.util.Timestamps;
import com.soulfiremc.grpc.generated.*;
import com.soulfiremc.server.SoulFireServer;
import com.soulfiremc.server.database.UserEntity;
import com.soulfiremc.server.user.AuthSystem;
import com.soulfiremc.server.user.PermissionContext;
import com.soulfiremc.server.util.RPCConstants;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.time.Instant;
import java.util.UUID;

@Slf4j
@RequiredArgsConstructor
public final class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
  private final SoulFireServer soulFireServer;

  private static void mutateOrThrow(UUID targetUser) {
    if (targetUser.equals(ServerRPCConstants.USER_CONTEXT_KEY.get().getUniqueId())) {
      throw new IllegalArgumentException("Cannot mutate self");
    } else if (targetUser.equals(AuthSystem.ROOT_USER_ID)) {
      throw new IllegalArgumentException("Cannot mutate root user");
    }
  }

  @Override
  public void createUser(UserCreateRequest request, StreamObserver<UserCreateResponse> responseObserver) {
    ServerRPCConstants.USER_CONTEXT_KEY.get().hasPermissionOrThrow(PermissionContext.global(GlobalPermission.CREATE_USER));

    try {
      soulFireServer.sessionFactory().inTransaction(session -> {
        var userEntity = new UserEntity();
        userEntity.id(UUID.randomUUID());
        userEntity.username(request.getUsername());
        userEntity.email(request.getEmail());
        userEntity.role(switch (request.getRole()) {
          case ADMIN -> UserEntity.Role.ADMIN;
          case USER -> UserEntity.Role.USER;
          case UNRECOGNIZED -> throw new IllegalArgumentException("Unknown role: " + request.getRole());
        });
        userEntity.minIssuedAt(Instant.now());

        session.persist(userEntity);
      });

      responseObserver.onNext(UserCreateResponse.newBuilder().build());
      responseObserver.onCompleted();
    } catch (Throwable t) {
      log.error("Error creating user", t);
      throw new StatusRuntimeException(Status.INTERNAL.withDescription(t.getMessage()).withCause(t));
    }
  }

  @Override
  public void deleteUser(UserDeleteRequest request, StreamObserver<UserDeleteResponse> responseObserver) {
    ServerRPCConstants.USER_CONTEXT_KEY.get().hasPermissionOrThrow(PermissionContext.global(GlobalPermission.DELETE_USER));

    try {
      var userId = UUID.fromString(request.getId());
      mutateOrThrow(userId);

      soulFireServer.authSystem().deleteUser(userId);

      responseObserver.onNext(UserDeleteResponse.newBuilder().build());
      responseObserver.onCompleted();
    } catch (Throwable t) {
      log.error("Error deleting user", t);
      throw new StatusRuntimeException(Status.INTERNAL.withDescription(t.getMessage()).withCause(t));
    }
  }

  @Override
  public void listUsers(UserListRequest request, StreamObserver<UserListResponse> responseObserver) {
    ServerRPCConstants.USER_CONTEXT_KEY.get().hasPermissionOrThrow(PermissionContext.global(GlobalPermission.READ_USER));

    try {
      var users = soulFireServer.sessionFactory().fromTransaction(session -> session.createQuery("from UserEntity", UserEntity.class).list());

      responseObserver.onNext(UserListResponse.newBuilder()
        .addAllUsers(users.stream().map(user -> {
            var result = UserListResponse.User.newBuilder()
              .setId(user.id().toString())
              .setUsername(user.username())
              .setEmail(user.email())
              .setRole(switch (user.role()) {
                case ADMIN -> UserRole.ADMIN;
                case USER -> UserRole.USER;
              })
              .setCreatedAt(Timestamps.fromMillis(user.createdAt().toEpochMilli()))
              .setUpdatedAt(Timestamps.fromMillis(user.updatedAt().toEpochMilli()))
              .setMinIssuedAt(Timestamps.fromMillis(user.minIssuedAt().toEpochMilli()));
            if (user.lastLoginAt() != null) {
              result.setLastLoginAt(Timestamps.fromMillis(user.lastLoginAt().toEpochMilli()));
            }

            return result.build();
          })
          .toList())
        .build());
      responseObserver.onCompleted();
    } catch (Throwable t) {
      log.error("Error listing users", t);
      throw new StatusRuntimeException(Status.INTERNAL.withDescription(t.getMessage()).withCause(t));
    }
  }

  @Override
  public void getUserInfo(UserInfoRequest request, StreamObserver<UserInfoResponse> responseObserver) {
    ServerRPCConstants.USER_CONTEXT_KEY.get().hasPermissionOrThrow(PermissionContext.global(GlobalPermission.READ_USER));

    try {
      var userId = UUID.fromString(request.getId());
      var user = soulFireServer.sessionFactory().fromTransaction(session -> session.find(UserEntity.class, userId));
      if (user == null) {
        throw new IllegalArgumentException("User not found: " + userId);
      }

      var result = UserInfoResponse.newBuilder()
        .setUsername(user.username())
        .setEmail(user.email())
        .setRole(switch (user.role()) {
          case ADMIN -> UserRole.ADMIN;
          case USER -> UserRole.USER;
        })
        .setCreatedAt(Timestamps.fromMillis(user.createdAt().toEpochMilli()))
        .setUpdatedAt(Timestamps.fromMillis(user.updatedAt().toEpochMilli()))
        .setMinIssuedAt(Timestamps.fromMillis(user.minIssuedAt().toEpochMilli()));
      if (user.lastLoginAt() != null) {
        result.setLastLoginAt(Timestamps.fromMillis(user.lastLoginAt().toEpochMilli()));
      }

      responseObserver.onNext(result.build());
      responseObserver.onCompleted();
    } catch (Throwable t) {
      log.error("Error getting user info", t);
      throw new StatusRuntimeException(Status.INTERNAL.withDescription(t.getMessage()).withCause(t));
    }
  }

  @Override
  public void invalidateSessions(InvalidateSessionsRequest request, StreamObserver<InvalidateSessionsResponse> responseObserver) {
    ServerRPCConstants.USER_CONTEXT_KEY.get().hasPermissionOrThrow(PermissionContext.global(GlobalPermission.INVALIDATE_SESSIONS));

    try {
      var userId = UUID.fromString(request.getId());
      mutateOrThrow(userId);

      soulFireServer.sessionFactory().inTransaction(session -> {
        var user = session.find(UserEntity.class, userId);
        if (user == null) {
          throw new IllegalArgumentException("User not found: " + userId);
        }

        user.minIssuedAt(Instant.now());

        session.merge(user);
      });

      responseObserver.onNext(InvalidateSessionsResponse.newBuilder().build());
      responseObserver.onCompleted();
    } catch (Throwable t) {
      log.error("Error getting user info", t);
      throw new StatusRuntimeException(Status.INTERNAL.withDescription(t.getMessage()).withCause(t));
    }
  }

  @Override
  public void updateUser(UpdateUserRequest request, StreamObserver<UpdateUserResponse> responseObserver) {
    ServerRPCConstants.USER_CONTEXT_KEY.get().hasPermissionOrThrow(PermissionContext.global(GlobalPermission.UPDATE_USER));

    try {
      var userId = UUID.fromString(request.getId());
      mutateOrThrow(userId);

      soulFireServer.sessionFactory().inTransaction(session -> {
        var user = session.find(UserEntity.class, userId);
        if (user == null) {
          throw new IllegalArgumentException("User not found: " + userId);
        }

        user.username(request.getUsername());
        user.email(request.getEmail());
        user.role(switch (request.getRole()) {
          case ADMIN -> UserEntity.Role.ADMIN;
          case USER -> UserEntity.Role.USER;
          case UNRECOGNIZED -> throw new IllegalArgumentException("Unknown role: " + request.getRole());
        });

        session.merge(user);
      });

      responseObserver.onNext(UpdateUserResponse.newBuilder().build());
      responseObserver.onCompleted();
    } catch (Throwable t) {
      log.error("Error updating user", t);
      throw new StatusRuntimeException(Status.INTERNAL.withDescription(t.getMessage()).withCause(t));
    }
  }

  @Override
  public void generateUserAPIToken(GenerateUserAPITokenRequest request, StreamObserver<GenerateUserAPITokenResponse> responseObserver) {
    ServerRPCConstants.USER_CONTEXT_KEY.get().hasPermissionOrThrow(PermissionContext.global(GlobalPermission.GENERATE_API_TOKEN));

    try {
      var userId = UUID.fromString(request.getId());
      mutateOrThrow(userId);

      var user = soulFireServer.sessionFactory().fromTransaction(session -> session.find(UserEntity.class, userId));
      if (user == null) {
        throw new IllegalArgumentException("User not found: " + userId);
      }

      var token = soulFireServer.authSystem().generateJWT(user, RPCConstants.API_AUDIENCE);

      responseObserver.onNext(GenerateUserAPITokenResponse.newBuilder()
        .setToken(token)
        .build());
      responseObserver.onCompleted();
    } catch (Throwable t) {
      log.error("Error generating user API token", t);
      throw new StatusRuntimeException(Status.INTERNAL.withDescription(t.getMessage()).withCause(t));
    }
  }
}
